<?php

require_once "interface.php";

class QLTermPage
{
	public function __construct ($query, &$node)
	{
		$this->query = $query;
		$this->node =& $node;
	} // end of __construct
	public function setPage ($v)
	{
		$node =& $this->getNode();
		$node['page'] = $v;
		return $this;
	} // end of method setPage
	public function setLength ($v)
	{
		$node =& $this->getNode();
		$node['length'] = $v;
		return $this;
	} // end of method setPage
	public function &getNode ()
	{
		if (!isset($this->node)) {
			$this->node['length'] = 1;
			$this->node['page'] = 1;
		}
		$this->node['id'] = 'pager';
		return $this->node;
	} // end of method getNode
}
class QLTermCount
{
	public function __construct ($query, &$node)
	{
		$this->query = $query;
		$this->node =& $node;
	} // end of __construct
	public function setCount ($v='count')
	{
		$node =& $this->getNode();
		if ($v) $node['alias'] = $v;
		return $this;
	} // end of method setPage
	public function &getNode ()
	{
		if (!isset($this->node)) {
			$this->node['id'] = 'count';
			$this->node['alias'] = 'count';
		}
		return $this->node;
	} // end of method getNode
}
class QLSelectQuery
{
	var $compiler;
	var $query;
	var $tree;
	var $params = array();

	public function __construct ($query)
	{
		$this->query = $query;
	} // end of __construct
	public function initalize ($env)
	{
		foreach ($env as $k=>$v) $this->$k = $v;
	} // end of method initalize
	public function getSQL ($params=array())
	{
		$ctx = new stdClass();
		$ctx->params = array_merge($this->getParams(), $params);
		$sql = $this->getCompiler()->compile($this->getTree(), $ctx);
		return $sql;
	} // end of method getQuery

	public function setPage ($page, $length=NULL)
	{
		$page = $this->compiler->parseConstant($page) or $page = $this->compiler->parseParam($page, array('integer'));
		$this->getTermPage()->setPage($page);
		if (isset($length)) {
			$length = $this->compiler->parseConstant($length) or $length = $this->compiler->parseParam($length, array('integer'));
			$this->getTermPage()->setLength($length);
		}
		return $this;
	} // end of method setPage
	public function setCount ($alias=NULL)
	{
		$this->getTermCount()->setCount($alias);
		return $this;
	} // end of method setPage
	public function &getTree()
	{
		if (!isset($this->tree)) {
			$this->tree = $this->compiler->parse($this->query);
		}
		return $this->tree;
	} // end of method getTree()
	public function getCompiler()
	{
		if (!isset($this->compiler)) {
			$this->compiler = new MysqlQLCompiler();
		}
		return $this->compiler;
	} // end of method getTree()
	public function setParam ($k,$v)
	{
		$this->data[$k] = $v;
	} // end of method setParam
	public function getParams()
	{
		return $this->data;
	} // end of method getTree()


	private function getTermPage ()
	{
		$tree =& $this->getTree();
		return new QLTermPage($this, $tree['query']['limits']);
	} // end of method getTermPage
	private function getTermCount ()
	{
		$tree =& $this->getTree();
		return new QLTermCount($this, $tree['query']['count']);
	} // end of method getTermPage
}

class MysqlQLCompiler
{

	private $keywords = array(
		'select', 'where', 'from',
		);
	public $store = NULL;
	public $error = NULL;
	public $cache = array();

	public function isKeyword ($name)
	{
		return in_array($name, $this->keywords);
	} // end of function isKeyword

	public function parseSql (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*\\[(.*?)\\]/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$node[   'id'] = 'sql';
			$node[ 'value'] = $matches[1];
			$result = $node;
		}
		return $result;
	} // end of function parseProperty
	public function parseConstant (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		$re  = '(([+-]?[0-9]+)([.][0-9]+)?)'                        // int or float
			.    "|(['](\\\\'|''|[^'])*?['])"                         // single quoted string
			.    '|(["](\\\\"|[^"])*["])';                            // double quoted string
		if (preg_match('/^\s*(?:'.$re.')/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			if (isset($matches[1]) && $matches[1] !== '') {           // it's float or int
				if (isset($matches[3]) && $matches[3] !== '') {         // it's float
					$node[   'id'] = 'float';
					$node['value'] = (float) $matches[1];
				} else {                                                // it's int
					$node[   'id'] = 'int';
					$node['value'] = (int) $matches[1];
				}
			} elseif (isset($matches[4]) && $matches[4] !== '') {
				$node[   'id'] = 'string';
				$node['value'] = preg_replace("/[^\\\\]\\\\'|''/", "'", substr($matches[4], 1, -1));
			} elseif (isset($matches[6]) && $matches[6] !== '') {
				$node[   'id'] = 'string';
				$node['value'] = preg_replace('/[^\\\\]\\\\"/', '"', substr($matches[6], 1, -1));
			}
			$result = $node;
		}
		return $result;
	} // end of function parseConstant
	public function parseProperty (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*([_a-zA-Z][_a-zA-Z0-9]*)(?:[.]([_a-zA-Z][_a-zA-Z0-9]*))?/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$node[   'id'] = 'property';
			$node['class'] = isset($matches[2]) && $matches[2] !== '' ? $matches[1] : 'objects';
			$node[ 'name'] = isset($matches[2]) && $matches[2] !== '' ? $matches[2] : $matches[1];
			$result = $node;
		}
		return $result;
	} // end of function parseProperty
	public function parseParam (&$query, $type_list)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*:([_a-zA-Z0-9]+)/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$node[  'id'] = 'param';
			$node['name'] = $matches[1];
			$node['types'] = $type_list;
			$result = $node;
		}
		return $result;
	} // end of function parseProperty
	public function parseClass (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*([_a-zA-Z][_a-zA-Z0-9]*)/si', $query, $matches) && !$this->isKeyword($matches[1])) {
			$query = substr($query, strlen($matches[0]));
			$node[  'id'] = 'class';
			$node['name'] = $matches[1];
			$result = $node;
		}
		return $result;
	} // end of function parseClass
	public function parseTerm (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if     ($result = $this->parseSql($query))                  {}
		elseif ($result = $this->parseConstant($query))             {}
		elseif ($result = $this->parseProperty($query))             {}
		elseif ($result = $this->parseParam($query, array('term'))) {}
		return $result;
	} // end of function parseTerm
	public function parseCmpExpr (&$query)
	{
		if ($this->error) return FALSE;
		$result = null;
		$right = $this->parseTerm($query);
		if (preg_match('/^\s*(in|==|!=|<=|>=|>|<|=~|!~|=%|!%|=)/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$left = $this->parseTerm($query);
			if ($left) {
				$node[      'id'] = 'cmp';
				$node['operator'] = $matches[1];
				$node[   'right'] = $right;
				$node[    'left'] = $left;
				$result = $node;
			}
		}
		return $result;
	} // end of function parseCmpExpr
	public function parseAndExpr (&$query)
	{
		if ($this->error) return FALSE;
		if ($result = $this->parseCmpExpr($query)) {}
		elseif ($result = $this->parseGroup($query)) {}
		if (preg_match('/^\s*and\s/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$left = $this->parseGroup($query);
			if ($left) {
				$node[   'id'] = 'and';
				$node['right'] = $result;
				$node[ 'left'] = $left;
				$result = $node;
			} else {
				$this->error = $query;
			}
		}
		return $result;
	} // end of function parseOrExpr
	public function parseOrExpr (&$query)
	{
		if ($this->error) return FALSE;
		$result = $this->parseAndExpr($query);
		if (preg_match('/^\s*or\s/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$left = $this->parseGroup($query);
			if ($left) {
				$node[   'id'] = 'or';
				$node['right'] = $result;
				$node[ 'left'] = $left;
				$result = $node;
			} else {
				$this->error = $query;
			}
		}
		return $result;
	} // end of function parseOrExpr
	public function parseGroup (&$query)
	{
		if ($this->error) return FALSE;
		$result = null;
		if (preg_match('/^\s*[(]/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			if (preg_match('/^\s*[)]/si', $query, $matches)) {
				// unexpected empty brackets
				$this->error = $query;
			} else {
				$right = $this->parseOrExpr($query);
				if (preg_match('/^\s*[)]/si', $query, $matches)) {
					$query = substr($query, strlen($matches[0]));
					$node[   'id'] = 'group';
					$node['right'] = $right;
					$result = $node;
				} else {
					$this->error = $query;
				}
			}
		} else {
			$result = $this->parseOrExpr($query);
		}
		return $result;
	} // end of function parseGroup
	public function parseWhat (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		$right = $this->parseClass($query);
		while($right) {
			$node[   'id'] = 'selectlist';
			$node[ 'left'] = $result;
			$node['right'] = $right;
			$right = NULL;
			if(preg_match('/^\s*,/si', $query, $matches)) {
				$query = substr($query, strlen($matches[0]));
				$right = $this->parseClass($query);
			}
			$result = $node;
		}
		return $result;
	} // end of function parseWhat
	public function parseWhere (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*where\s*/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$result = $this->parseOrExpr($query);
		}
		return $result;
	} // end of function parseWhere
	public function parseOrder (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*order\s+by/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$property = $this->parseProperty($query);
			while($property) {
				if(preg_match('/^\s*(ASC|DESC)/si', $query, $matches)) {
					$query = substr($query, strlen($matches[0]));
					$sort_type = strtoupper($matches[1]);
				} else {
					$sort_type = 'ASC';
				}
				$node[  'id'] = 'orderbyproperty';
				$node[ 'left'] = $result;
				$node[ 'type'] = $sort_type;
				$node['right'] = $property;
				$property = NULL;
				if(preg_match('/^\s*,/si', $query, $matches)) {
					$query = substr($query, strlen($matches[0]));
					$property = $this->parseProperty($query);
				}
				$result = $node;
			}
		}
		return $result;
	} // end of function parseOrder
	public function parseLimits (&$query)
	{
		$result = $this->parseLimit($query) or
			$result = $this->parsePage($query);
		return $result;
	} // end of function parseLimits
	public function parseLimit (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*limit\s/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$a = NULL; $b = NULL;
			if (($a = $this->parseConstant($query)) || ($a=$this->parseParam($query, array('integer')))) {
				if (preg_match('/^\s*,/si', $query, $matches)) {
					$query = substr($query, strlen($matches[0]));
					if (($b = $this->parseConstant($query)) || ($b=$this->parseParam($query, array('integer')))) {
					} else {
						$this->error = $query;
					}
				}
				$node[    'id'] = 'limit';
				$node[ 'count'] = isset($b) ? $b : $a;
				$node['offset'] = isset($b) ? $a : NULL;
				$result = $node;
			} else {
				$this->error = $query;
			}
		}
		return $result;
	} // end of function parseLimit
	public function parsePage (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*page\s/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$a = NULL; $b = NULL;
			if (($a = $this->parseConstant($query)) || ($a=$this->parseParam($query, array('integer')))) {
				if (preg_match('/^\s*,/si', $query, $matches)) {
					$query = substr($query, strlen($matches[0]));
					if (($b = $this->parseConstant($query)) || ($b=$this->parseParam($query, array('integer')))) {
					} else {
						$this->error = $query;
					}
				}
				$node[    'id'] = 'pager';
				$node['length'] = isset($b) ? $b : $a;
				$node[  'page'] = isset($b) ? $a : 1;
				$result = $node;
			} else {
				$this->error = $query;
			}
		}
		return $result;
	} // end of function parsePage
	public function parseCount (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*count(\s+as\s+([_a-zA-Z][_a-zA-Z0-9]*))?/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$node[    'id'] = 'count';
			$node[ 'alias'] = isset($matches[2]) ? $matches[2] : 'count';
			$result = $node;
		}
		return $result;
	} // end of function parseCount
	public function parseSelect (&$query)
	{
		if ($this->error) return FALSE;
		$result = NULL;
		if (preg_match('/^\s*(select)?/si', $query, $matches)) {
			$query = substr($query, strlen($matches[0]));
			$node[    'id'] = 'select';
			$node[ 'count'] = $this->parseCount($query);
			$node[  'what'] = $this->parseWhat($query);
			$node[ 'where'] = $this->parseWhere($query);
			$node[ 'order'] = $this->parseOrder($query);
			$node['limits'] = $this->parseLimits($query);
			$result = $node;
		}
		return $result;
	} // end of function parseSelect
	public function parseQuery (&$query)
	{
		$result = NULL;
		if (is_string($query)) {
			if ($node = $this->parseSelect($query)) {
			//} elseif ($tree = $this->parseDelete($query)) {
			}
			if ($node) {
				$result[   'id'] = 'query';
				$result['query'] = $node;
			}
		}
		return $result;
	} // end of function parseQuery

	function parse ($query)
	{
		$result = NULL;
		if (array_key_exists($query, $this->cache)) {
			$result = $this->cache[$query];
		} else {
			$node = $this->parseQuery($query);
			if (trim($query)) { // остались неразобранные символы
				$this->error = $query;
				$result = NULL;
			} else {
				$result = $this->cache[$query] = $node; 
			}
		}
		return $result;
	}
	public function compile ($node, $ctx=NULL)
	{
		$res = NULL;
		switch ($node['id'])
		{
		case 'query':
			$ctx->tables = array();
			$ctx->fields = array();
			$ctx->limits = array();
			$ctx->order = array();
			$res = $this->compile($node['query'], $ctx);
			break;
		case 'select':
			$this->compile($node['what'], $ctx);
			if (empty($ctx->fields)) $this->compile($this->parseWhat($query = 'objects'), $ctx);
			if (isset($node['from'])) $this->compile($node['from'], $ctx);
			$ewhere = $this->compile($node['where'], $ctx);
			if ($ewhere) $ewhere = '('.$ewhere.')';
			if (isset($node['order'])) $this->compile($node['order'], $ctx);
			if (isset($node['limits'])) $this->compile($node['limits'], $ctx);
			$target_tables = $ctx->target_tables;

			$etables = '';
			foreach($ctx->tables as $k=>$v) {
				if ($etables) {
					$etables .= ','. $v;
				} else {
					$etables = $v;
				}
				if (in_array($v, $target_tables)) continue;
				foreach ($target_tables as $target_table) {
					$ewhere .= ($ewhere ? ' and ' : '') . "$v.oid=$target_table.oid";
				}
			}
			$res = (
				empty($node['count']) ? 'SELECT DISTINCT ' . implode(', ', $ctx->fields) . "\n"
				                      : 'SELECT COUNT(DISTINCT ' .$ctx->target_tables[0] . ".oid) AS `{$node['count']['alias']}`\n"
				)
				.    ' FROM '.$etables."\n"
				.      ($ewhere  ? ' WHERE ' . $ewhere ."\n": '')
				.      ($ctx->order  ? ' ORDER BY ' . implode(',', $ctx->order) ."\n": '')
				.      ($ctx->limits ? ' LIMIT ' . $ctx->limits ."\n": '')
				;
			break;
		case 'sql':
			$res = $node['value'];
			break;
		case 'or':
			$left  = $this->compile($node['left'], $ctx);
			$right = $this->compile($node['right'], $ctx);
			$res   = "$right OR $left";
			break;
		case 'and':
			$left  = $this->compile($node['left'], $ctx);
			$right = $this->compile($node['right'], $ctx);
			$res   = "$right AND $left\n";
			break;
		case 'group':
			$left  = $this->compile($node['left'], $ctx);
			$right = $this->compile($node['right'], $ctx);
			$res   = "($right)$left\n";
			break;
		case 'cmp':
			$left  = $this->compile($node['left'], $ctx);
			$right = $this->compile($node['right'], $ctx);
			$not = '';
			switch ($node['operator']) {
			case '=':
			case '==':
				$eop = '=';
				break;
			case 'in':
			case 'IN':
				$eop = 'IN';
				break;
			case '!=':
			case '<=':
			case '>=':
			case '>':
			case '<':
				$eop = $node['operator'];
				break;
			case '!%':
				$not = 'NOT ';
			case '=%':
				$eop = $not.'LIKE';
				break;
			case '!~':
				$not = 'NOT ';
			case '=~':
				$eop = $not.'REGEXP';
			}
			$res   = "$right $eop $left";
			break;
		case 'int':
		case 'float':
			$res = $node['value'];
			break;
		case 'string':
			$res = $this->store->db->quote($node['value']);
			break;
		case 'property':
			$etable = $node['class'] === 'objects'
				?       $this->store->db->quoteTable($node['class'])
				:       $this->store->quoteClass($node['class']);
			$ctx->tables[$etable] = $etable;
			$efield = $this->store->db->quoteField($node['name']);
			$res = "$etable.$efield";
			break;
		case 'param':
			if (isset($ctx->params[$node['name']])) {
				$value = $ctx->params[$node['name']];
				$res = NULL;
				while ((list($k, $type) = each($node['types'])) && !isset($res)) {
					switch ($type) {
					case 'term': 
						//$res = $this->compile($this->parseTerm($value), $ctx); 
						$res = is_array($value) 
							?    '('. implode(',', array_map(array($this->store->db, 'quote'), $value)).')'
							:    $this->store->db->quote($value);
						break;
					case 'integer': 
						//$res = $this->compile($this->parseTerm($value), $ctx); 
						$res = intval($value);
						break;
					}
				}
				if (!isset($res)) $res = $this->store->db->quote($value);
			} else {
				$res = $this->store->db->quote('');
			}
			break;
		case 'orderbyproperty':
			$res = $this->compile($node['left'], $ctx);
			$ctx->order[] = $this->compile($node['right'], $ctx) . ' ' . $node['type'];
			break;
		case 'selectlist':
			$target_table = $this->compile($node['right'], $ctx);
			$ctx->fields[] = $target_table.'.*';
			$ctx->target_tables[] = $target_table;
			$res = isset($node['left']) ? $this->compile($node['left'], $ctx) : '';
			break;
		case 'class':
			$etable = $node['name'] === 'objects'
				?       $this->store->db->quoteTable($node['name'])
				:       $this->store->quoteClass($node['name']);
			$ctx->tables[$etable] = $etable;
			$res = $etable;
			break;
		case 'limit':
			$offset = $this->compile($node['offset'], $ctx);
			$count = $this->compile($node['count'], $ctx);
			if (isset($offset)) {
				$count  = isset($count) ? $count : '-1';
				$offset = $offset;
				$res    = "$offset,$count";
			} else {
				$res = $count;
			}
			$ctx->limits = $res;
			break;
		case 'pager':
			$page = $this->compile($node['page'], $ctx);
			$length = $this->compile($node['length'], $ctx);

			if ($page < 1) $page = 1;

			if ($page) {
				$offset = ($page - 1) * $length;
				$res    = "$offset,$length";
			} else {
				$res    = "$length";
			}
			$ctx->limits = $res;
			break;
		default:
			//die("Unknown node type '" . $node['id']."'");
		}
		return $res;
	} // end of function compile

}
